feat: add admin-to-user and user-to-user communication via gift wraps#132
Conversation
- Add dmtouser command: send gift wrapped messages between users using trade keys from order IDs - Add admsenddm command: admin can send gift wrapped messages - Add getdmuser command: view gift wrapped messages received on trade keys and admin key - Filter messages by sender: getdm shows only Mostro messages (include admin messages), getdmuser shows only user messages New commands: - mostro-cli dmtouser -p <pubkey> -o <order_id> -m <message> - mostro-cli admsenddm -p <pubkey> -m <message> - mostro-cli getdmuser -s <minutes>
WalkthroughAdds CLI commands for admin-to-user and order-tied user-to-user DMs and a user-focused DM retrieval command; threads a Mostro public key through DM retrieval, introduces gift-wrap send/get helpers, and adds a DB accessor to collect distinct trade keys. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant CLI as CLI (DmToUser)
participant DB as DB (orders)
participant Util as util.rs
participant Nostr as Nostr Client
rect rgba(230,245,255,0.6)
note right of User: DmToUser flow
User->>CLI: CLI DmToUser --pubkey --order_id --message
CLI->>DB: Order::get_by_id(order_id)
DB-->>CLI: Order { trade_keys }
CLI->>Util: send_gift_wrap_dm(trade_keys, receiver, message)
Util->>Nostr: gift_wrap + send
Nostr-->>Util: ack
Util-->>CLI: Ok
CLI-->>User: "DM sent"
end
sequenceDiagram
autonumber
actor Admin
participant CLI as CLI (AdmSendDm)
participant Env as Env (NSEC_PRIVKEY)
participant Util as util.rs
participant Nostr as Nostr Client
rect rgba(255,240,230,0.6)
note right of Admin: AdmSendDm flow
Admin->>CLI: CLI AdmSendDm --pubkey --message
CLI->>Env: read NSEC_PRIVKEY
Env-->>CLI: admin_keys
CLI->>Util: send_admin_gift_wrap_dm(admin_keys, receiver, message)
Util->>Nostr: gift_wrap + send
Nostr-->>Util: ack
Util-->>CLI: Ok
CLI-->>Admin: "Admin DM sent"
end
sequenceDiagram
autonumber
actor Operator
participant CLI as CLI (GetDmUser)
participant DB as DB (orders)
participant Util as util.rs
participant Nostr as Nostr Client
rect rgba(235,255,235,0.6)
note right of Operator: GetDmUser flow
Operator->>CLI: CLI GetDmUser --since
CLI->>DB: Order::get_all_trade_keys()
DB-->>CLI: Vec<trade_keys_hex>
CLI->>Util: get_direct_messages_from_trade_keys(keys, since, mostro_pk)
Util->>Nostr: fetch GiftWrap events
Nostr-->>Util: events
Util-->>CLI: Vec<(Message, ts, sender_pk)>
CLI-->>Operator: Render table
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/util.rs (1)
376-381: Bug: Hardcoded 30-minute filter instead of using thesinceparameter.The function uses a hardcoded 30-minute filter instead of the
sinceparameter passed to the function.// Here we discard messages older than the real since parameter let since_time = chrono::Utc::now() - .checked_sub_signed(chrono::Duration::minutes(30)) + .checked_sub_signed(chrono::Duration::minutes(since)) .unwrap() .timestamp() as u64; if created_at.as_u64() < since_time { continue; }
🧹 Nitpick comments (7)
src/db.rs (1)
442-461: Harden get_all_trade_keys: drop empty entries and prep for scale (index).
- Filter out empty strings to avoid useless fetches.
- Consider adding an index on orders.trade_keys to make DISTINCT/IS NOT NULL fast.
Apply this diff within this function to skip empties:
- let trade_keys: Vec<String> = rows - .into_iter() - .filter_map(|row| row.trade_keys) - .collect(); + let trade_keys: Vec<String> = rows + .into_iter() + .filter_map(|row| row.trade_keys) + .filter(|s| !s.trim().is_empty()) + .collect();Additionally, create an index during DB setup (outside this hunk, in connect()):
sqlx::query( r#" CREATE TABLE IF NOT EXISTS orders ( ... ); CREATE TABLE IF NOT EXISTS users ( ... ); + CREATE INDEX IF NOT EXISTS idx_orders_trade_keys ON orders(trade_keys); "#, )src/cli/adm_send_dm.rs (1)
19-25: Use structured logging for non-errors; stderr for errors.Keep stdout clean for command output; prefer info!/warn!/eprintln!.
- println!("SENDING DM with admin keys: {}", admin_keys.public_key().to_hex()); + info!("Sending DM with admin pubkey: {}", admin_keys.public_key().to_hex()); ... - println!("Admin gift wrap message sent to {}", receiver); + println!("Admin gift wrap message sent to {}", receiver);src/cli/get_dm_user.rs (2)
15-18: Validate admin key before adding; avoid silent skip later.Parse now so a bad env var fails loudly.
- if let Ok(admin_privkey_hex) = std::env::var("NSEC_PRIVKEY") { - trade_keys_hex.push(admin_privkey_hex); - } + if let Ok(admin_privkey_hex) = std::env::var("NSEC_PRIVKEY") { + if Keys::parse(&admin_privkey_hex).is_ok() { + trade_keys_hex.push(admin_privkey_hex); + } else { + eprintln!("Ignoring invalid NSEC_PRIVKEY value (failed to parse)"); + } + }
56-57: Pass owned Strings to comfy_table to avoid unnecessary refs.Cleaner and avoids potential trait-coercion quirks.
- table.add_row(vec![&formatted_date, &sender_hex, &message_str]); + table.add_row(vec![formatted_date, sender_hex, message_str]);src/cli.rs (3)
168-173: Consider adding validation for thesinceparameter.The
sinceparameter represents minutes and should have reasonable bounds to prevent excessive data fetching or negative values that could cause unexpected behavior.Consider adding validation either in the clap definition or in the execute function:
GetDmUser { /// Since time of the messages in minutes #[arg(short, long)] #[clap(default_value_t = 30)] + #[clap(value_parser = clap::value_parser!(i64).range(0..=10080))] // Max 1 week since: i64, },
196-207: Improve documentation clarity for gift-wrapped messages.The command description should clarify that this uses gift-wrapped messages with ephemeral keys for privacy, distinguishing it from the regular
SendDmcommand.- /// Send gift wrapped direct message to a user + /// Send gift-wrapped direct message to a user using ephemeral trade keys from an order DmToUser {
517-524: Consider extracting duplicate pubkey parsing logic.The pubkey parsing from string is duplicated across multiple command handlers. Consider extracting this to a helper function.
Add a helper function to reduce duplication:
+fn parse_pubkey(pubkey_str: &str) -> Result<PublicKey> { + PublicKey::from_str(pubkey_str) +} Commands::DmToUser { pubkey, order_id, message, } => { - let pubkey = PublicKey::from_str(pubkey)?; + let pubkey = parse_pubkey(pubkey)?; execute_dm_to_user(pubkey, &client, order_id, message).await? }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (7)
src/cli.rs(7 hunks)src/cli/adm_send_dm.rs(1 hunks)src/cli/dm_to_user.rs(1 hunks)src/cli/get_dm.rs(2 hunks)src/cli/get_dm_user.rs(1 hunks)src/db.rs(1 hunks)src/util.rs(3 hunks)
🧰 Additional context used
🧬 Code graph analysis (7)
src/db.rs (1)
src/cli/send_msg.rs (1)
create_next_trade_payload(85-105)
src/cli/dm_to_user.rs (2)
src/util.rs (3)
send_gift_wrap_dm(48-79)None(32-32)None(65-65)src/db.rs (2)
connect(12-72)get_by_id(417-433)
src/cli/adm_send_dm.rs (1)
src/util.rs (2)
send_admin_gift_wrap_dm(15-46)var(91-93)
src/cli/get_dm.rs (1)
src/util.rs (1)
get_direct_messages(285-390)
src/cli/get_dm_user.rs (2)
src/util.rs (1)
get_direct_messages_from_trade_keys(208-283)src/db.rs (4)
connect(12-72)get_all_trade_keys(442-460)new(84-105)new(225-290)
src/cli.rs (5)
src/cli/adm_send_dm.rs (1)
execute_adm_send_dm(5-26)src/cli/dm_to_user.rs (1)
execute_dm_to_user(6-31)src/cli/get_dm.rs (1)
execute_get_dm(11-117)src/cli/get_dm_user.rs (1)
execute_get_dm_user(9-62)src/cli/send_dm.rs (1)
execute_send_dm(7-38)
src/util.rs (3)
src/cli/send_dm.rs (1)
execute_send_dm(7-38)src/cli/take_dispute.rs (3)
execute_admin_settle_dispute(75-105)execute_take_dispute(107-142)execute_admin_add_solver(8-41)src/cli/send_msg.rs (1)
create_next_trade_payload(85-105)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (5)
src/cli/get_dm.rs (1)
17-18: Ensure callers includemostro_pubkeyand confirm the 30-minute cutoff
- All
execute_get_dm(...)invocations insrc/cli.rs(lines 410 and 416) now pass&mostro_keyas the final argument, so the new parameter is wired through correctly.- In
src/util.rs(around line 376), the code still uses a hardcoded 30-minute window (chrono::Duration::minutes(30)) when discarding old messages. Please confirm whether this fixed cutoff is intentional or if it should instead use thesinceparameter passed intoget_direct_messages.src/cli.rs (2)
2-6: LGTM! Well-organized module structure.The new modules for DM operations are properly organized and follow the existing naming convention.
410-414: Good architectural design with the mostro_key parameter.Passing
mostro_keyto the DM retrieval functions enables proper filtering between admin and user messages, maintaining clear separation of concerns.src/util.rs (2)
200-200: Good backwards compatibility maintenance.The update to pass
Nonefor the newmostro_pubkeyparameter maintains backwards compatibility with existing code.
361-367: Well-implemented Mostro message filtering.The filtering logic for Mostro messages is clean and maintains backwards compatibility with the optional parameter.
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (1)
src/util.rs (1)
53-61: Validate the admin key before sending admin DMs.Enforce an optional check against a configured admin pubkey to prevent accidental misuse; avoid leaking full keys in logs.
pub async fn send_admin_gift_wrap_dm( client: &Client, admin_keys: &Keys, receiver_pubkey: &PublicKey, message: &str, ) -> Result<()> { + // Optional hardening: if EXPECTED_ADMIN_PUBKEY is set, enforce it + if let Ok(expected_hex) = var("EXPECTED_ADMIN_PUBKEY") { + let expected = PublicKey::from_hex(&expected_hex)?; + anyhow::ensure!( + admin_keys.public_key() == expected, + "Provided key does not match EXPECTED_ADMIN_PUBKEY" + ); + } send_gift_wrap_dm_internal(client, admin_keys, receiver_pubkey, message, true).await }
🧹 Nitpick comments (8)
src/cli.rs (4)
167-173: Clarify GetDmUser help to reflect user-only messagesCurrent help text is vague. Make it explicit that this excludes Mostro/admin messages to avoid confusion with GetDm.
- /// Get direct messages sent to any trade keys + /// Get user-to-user (gift-wrapped) DMs received on your trade keys (excludes Mostro/admin messages)
196-207: Disambiguate DmToUser vs SendDm in help textSpell out that order_id’s trade keys are used for gift-wrap, so users pick this over SendDm when appropriate.
- /// Send gift wrapped direct message to a user + /// Send gift-wrapped direct message to a user using the order's trade keys @@ - /// Order id to get ephemeral keys + /// Order id whose trade keys will be used for gift-wrapping
269-277: Document admin key requirement for AdmSendDmAdd a note in the help text that NSEC_PRIVKEY must be set. The runtime check is good; this improves UX.
- /// Send gift wrapped direct message to a user (only admin) + /// Send gift-wrapped direct message to a user (only admin; requires NSEC_PRIVKEY)
433-440: Good switch to anyhow::bail for missing NSEC_PRIVKEY; consider DRY helperNice improvement over printing and exiting. To reduce repetition across admin arms, introduce a small helper.
Add once (outside match):
fn admin_keys() -> anyhow::Result<Keys> { match std::env::var("NSEC_PRIVKEY") { Ok(k) => Ok(Keys::parse(&k)?), Err(e) => anyhow::bail!("NSEC_PRIVKEY not set: {e}"), } }Then update each arm (example shown; apply similarly to others):
- let id_key = match std::env::var("NSEC_PRIVKEY") { - Ok(id_key) => Keys::parse(&id_key)?, - Err(e) => { - anyhow::bail!("NSEC_PRIVKEY not set: {e}"); - } - }; + let id_key = admin_keys()?;Also applies to: 476-479, 486-489, 496-499
src/util.rs (4)
22-25: Emit a warning when POW is invalid; mirror this in send_dm.Currently defaults silently to 0. Log once to aid ops/debug. Also replicate the safe parsing in send_dm (Line 80) to avoid panic.
- let pow: u8 = var("POW") - .unwrap_or_else(|_| "0".to_string()) - .parse() - .unwrap_or(0); + let pow: u8 = var("POW") + .ok() + .and_then(|s| s.parse::<u8>().ok()) + .unwrap_or_else(|| { + log::warn!("Invalid POW env var, defaulting to 0"); + 0 + });Outside this hunk (send_dm, Line 80), replace:
let pow: u8 = var("POW").unwrap_or('0'.to_string()).parse().unwrap();with the same safe+warn parsing shown above.
215-219: Clamp/validate the since window to avoid surprises with negatives/overflow.Protect against negative or extreme inputs and keep behavior predictable.
- let since_time = chrono::Utc::now() - .checked_sub_signed(chrono::Duration::minutes(since)) + let since = since.max(0).min(60 * 24 * 30); // 0..=30 days + let since_time = chrono::Utc::now() + .checked_sub_signed(chrono::Duration::minutes(since)) .unwrap() .timestamp() as u64;
225-231: Consider adding a reasonable limit to the filter.Without a limit, relays may return large result sets. A modest cap keeps queries bounded.
- let filters = Filter::new() + let filters = Filter::new() .kind(nostr_sdk::Kind::GiftWrap) .pubkey(trade_keys.public_key()) - .since(fake_timestamp); + .since(fake_timestamp) + .limit(500);
356-363: Filter logic is correct; also align time filtering with the caller-provided since.The Mostro-only filter looks right. Consider replacing the hardcoded 30-minute cutoff below (Lines 371–374) with the since parameter for consistency with getdm/getdmuser semantics.
Example (outside this hunk):
// Replace fixed 30 min window with the since parameter let since_time = chrono::Utc::now() .checked_sub_signed(chrono::Duration::minutes(since)) .unwrap() .timestamp() as u64;
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (5)
src/cli.rs(11 hunks)src/cli/adm_send_dm.rs(1 hunks)src/cli/dm_to_user.rs(1 hunks)src/cli/get_dm.rs(2 hunks)src/util.rs(3 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- src/cli/get_dm.rs
- src/cli/adm_send_dm.rs
- src/cli/dm_to_user.rs
🧰 Additional context used
🧬 Code graph analysis (2)
src/cli.rs (5)
src/cli/adm_send_dm.rs (1)
execute_adm_send_dm(5-24)src/cli/dm_to_user.rs (1)
execute_dm_to_user(6-27)src/cli/get_dm.rs (1)
execute_get_dm(11-117)src/cli/get_dm_user.rs (1)
execute_get_dm_user(9-62)src/cli/send_dm.rs (1)
execute_send_dm(7-38)
src/util.rs (2)
src/db.rs (2)
new(84-105)new(225-290)src/cli/send_dm.rs (1)
execute_send_dm(7-38)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (12)
src/cli.rs (7)
2-6: New modules correctly exposedThe wiring for adm_send_dm, dm_to_user, and get_dm_user looks consistent with the new features.
19-23: Execute imports are correctly hooked upImports align with the new modules and are used below in run().
412-414: GetDmUser dispatch is correctArguments and mostro_key threading match execute_get_dm_user signature.
416-417: GetAdminDm correctly threads mostro_keySignature change is applied consistently here.
513-520: DmToUser dispatch looks correctType usage (PublicKey::from_str, &Uuid) and argument order match execute_dm_to_user.
521-524: AdmSendDm dispatch looks correctParses pubkey and forwards to executor as expected.
410-411: The script to verify theexecute_get_dmsignature and theGetDmvariant has been initiated. I’ll await the results to confirm the necessary changes.src/util.rs (5)
15-51: Good refactor: consolidated GiftWrap DM sending + safe POW parsing.Deduplicates logic, uses non-panicking POW parsing, and standardizes JSON payload ((Message, None)). Looks clean.
62-69: LGTM: user GiftWrap sender delegates to the shared helper.Keeps behavior consistent across admin/user senders.
190-190: LGTM: callsite updated for new get_direct_messages signature.Passing None for mostro_pubkey is correct in the user-to-user flow.
198-279: Solid implementation for multi-trade-key DM retrieval.Good use of HashSet for dedup, error logging, and Mostro filtering. Sorted output is deterministic.
286-286: Callsites updated for newmostro_pubkeyparameterI’ve verified that all three invocations of
get_direct_messagesnow include the new fifth argument, matching the intended usage:
src/util.rs (around line 190):
get_direct_messages(client, trade_keys, 15, to_user, None).await– passes
Nonewhen nomostro_pubkeyis needed.src/cli/get_dm.rs (line 24):
let dm_temp = get_direct_messages(client, &keys, *since, from_user, Some(mostro_pubkey)).await;– passes
Some(mostro_pubkey)for CLI use.src/cli/get_dm.rs (line 35):
let dm_temp = get_direct_messages(client, &id_key, *since, from_user, Some(mostro_pubkey)).await;– likewise passes
Some(mostro_pubkey).These match the intended semantics (omitted in the GiftWrap branch, provided in CLI). No further changes needed.
New commands:
Summary by CodeRabbit
New Features
Improvements